package meatslope2.gameengine.engine;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import javax.microedition.khronos.opengles.GL10;

import meatslope2.constants.TimeConstants;
import meatslope2.gameengine.input.InputSystem;

/**
 * Shamelessly based on AndEngine's engine
 * https://github.com/nicolasgramlich/AndEngine/
 * <br><br>
 * 
 * Contains a static {@link Time} class that is accessible to get total elapsed time/delta time.
 * <br><br>
 * 
 * {@link Engine#draw()} will add any drawing for game objects.  
 * {@link Engine#update()} will add any updating for game objects.
 * <br>
 * These two functions are run mutually exclusive by {@link UpdateThread} and GL-{@link Thread} via
 * the {@link EngineLock}
 * @author Mitchell Thelen
 */
public abstract class Engine
{
	// ===========================================================
	// Members
	// ===========================================================
	private final EngineLock engineLock;
	private final UpdateThread updateThread;
	protected final InputSystem inputSystem;
	
	private int width;
	private int height;
	
	// ===========================================================
	// Getters/Setters
	// ===========================================================
	public void setWindow(int w, int h)
	{
		this.width = w;
		this.height = h;
	}
	
	public int getWidth()
	{
		return this.width;
	}
	
	public int getHeight()
	{
		return this.height;
	}
	
	// ===========================================================
	// Constructors
	// ===========================================================
	public Engine(InputSystem inputSystem)
	{	
		assert inputSystem != null : "incoming input system cannot be null";

		this.inputSystem = inputSystem;
		this.engineLock = new EngineLock();
		this.updateThread = new UpdateThread(this);
	}
	
	// ===========================================================
	// Methods for/from SuperClass/Interfaces
	// ===========================================================
	
	/**
	 * Starts game engine/update cycle
	 */
	public void start()
	{
        // Now's a good time to run the GC.  Since we won't do any explicit
        // allocation during the game, the GC should stay dormant and not
        // influence our gameplay.
        Runtime r = Runtime.getRuntime();
        r.gc();
        
		Time.start();
		this.updateThread.start();
	}
	
	/**
	 * Main update cycle
	 * @throws InterruptedException
	 */
	protected void update() throws InterruptedException 
	{
		Time.update();
		consumeEvents();
	}
	
	/**
	 * Main draw cycle
	 * @throws InterruptedException
	 */
	protected abstract void draw(final GL10 pGL) throws InterruptedException;
	
	protected abstract void consumeEvents() throws InterruptedException;

	// ===========================================================
	// Methods
	// ===========================================================
	
	/**
	 * Draw cycle called by GL-{@link Thread} to execute {@link Engine#draw()} thread-safely
	 * @throws InterruptedException
	 */
	public void onDrawFrame(final GL10 pGL) throws InterruptedException
	{
		this.engineLock.lock();
		try 
		{
			engineLock.waitUntilCanDraw();

			this.draw(pGL);

			engineLock.notifyCanUpdate();
		} 
		finally 
		{
			engineLock.unlock();
		}
	}
	
	/**
	 * Update cycle called by {@link UpdateThread} to execute {@link Engine#update()} thread-safely
	 * @throws InterruptedException
	 */
	private void onTickUpdate() throws InterruptedException 
	{
		this.engineLock.lock();
		try 
		{
			this.update();
			
			this.engineLock.notifyCanDraw();
			this.engineLock.waitUntilCanUpdate();
		}
		finally 
		{
			this.engineLock.unlock();
		}
	}
	
	/**
	 * Responsible for the game's update cycle.  Calls {@link Engine#onTickUpdate()} until interrupted
	 * 
	 * @author Mitchell Thelen
	 */
	private static class UpdateThread extends Thread 
	{
		private final Engine engine;

		public UpdateThread(Engine engine) 
		{
			super(UpdateThread.class.getSimpleName());
			this.engine = engine;
		}
		
		@Override
		public void run() 
		{
			try 
			{
				while(true) 
				{
					this.engine.onTickUpdate();
				}
			} 
			catch (final InterruptedException e) 
			{
				//This is expected
				this.interrupt();
			}
		}
	}
	
	/**
	 * The {@link EngineLock} can be used to {@link EngineLock#lock()}/{@link EngineLock#unlock()} on, to ensure the code in between runs mutually exclusive to the {@link UpdateThread} and the GL{@link Thread}.
	 * When the caller already is on the {@link UpdateThread} or the GL-{@link Thread}, that code is executed immediately.
	 * 
	 * @author Mitchell Thelen
	 *
	 */
	private static class EngineLock extends ReentrantLock 
	{
		private static final long serialVersionUID = 671220941302523934L;

		private final Condition drawingCondition = this.newCondition();
		private final AtomicBoolean drawing = new AtomicBoolean(false);

		/**
		 * Constructs an {@link ReentrantLock} with the fairness parameter set to false
		 */
		public EngineLock() 
		{
			super(false);
		}

		/**
		 * Used by the {@link UpdateThread} to notify the GL-{@link Thread} that it may execute its
		 * code safely
		 */
		private void notifyCanDraw() 
		{
			this.drawing.set(true);
			this.drawingCondition.signalAll();
		}

		/**
		 * Used by the {@link UpdateThread} to wait until the GL-{@link Thread} has finished executing its
		 */
		private void waitUntilCanUpdate() throws InterruptedException 
		{
			while(this.drawing.get())
				this.drawingCondition.await();
		}

		/**
		 * Used by the GL-{@link Thread} to notify the {@link UpdateThread} that it may execute its
		 * code safely
		 */
		void notifyCanUpdate() 
		{
			this.drawing.set(false);
			this.drawingCondition.signalAll();
		}

		/**
		 * Used by the GL-{@link Thread} to wait until the {@link UpdateThread} has finished executing its
		 */
		void waitUntilCanDraw() throws InterruptedException 
		{
			while(!this.drawing.get())
				this.drawingCondition.await();
		}
	}
	
	/**
	 * Consolidates all time information. The {@link Time#update()} method needs to be called once per 
	 * update cycle. The {@link Time#start()} method needs to be called when the {@link Engine} and
	 * {@link UpdateThread} starts.  Then its public members ({@link Time#getTotalTime()}, {@link Time#getDeltaTime})
	 * are usable throughout the application
	 * 
	 * @author Mitchell Thelen
	 */
	public static class Time
	{
		private static long lastTick = 0;
		
		private static float totalTime = 0;
		private static float deltaTime = 0;
		
		/**
		 * @return total time since the game has started in seconds
		 */
		public static float getTotalTime() 
		{
			return totalTime;
		}
		
		/**
		 * @return total time since last update cycle
		 */
		public static float getDeltaTime()
		{
			return deltaTime;
		}
		
		/**
		 * Called in {@link Engine#start()}.  Sets the last tick time to now, or
		 * {@link System#nanoTime()} so the {@link Time#getDeltaTime()} is not artificially high
		 * initially.
		 */
		private static void start()
		{
			lastTick = System.nanoTime();
		}
		
		/**
		 * Called at beginning of {@link Engine#update()} cycle.
		 */
		private static void update()
		{
			//find out how many nanoseconds happened, add them to the last tick
			final long ticksElapsed = System.nanoTime() - lastTick;
			lastTick += ticksElapsed;
			
			//convert ticks to seconds, assign to delta time
			deltaTime = ticksElapsed * TimeConstants.SECONDS_PER_NANOSECOND;
			
			//append elapsed seconds to total seconds
			totalTime += deltaTime;
		}
	}
}
